package com.sinai.mshab.client.widgets;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.FocusListener;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sinai.mshab.client.formatting.BlankChecker;
import com.sinai.mshab.client.formatting.Container;
import com.sinai.mshab.client.formatting.Direction;
import com.sinai.mshab.client.formatting.FormatPanelDefaults;
import com.sinai.mshab.client.formatting.VisibilityManager;

/**
 * Creates a panel that can accept formatting of it's widgets. Supports default
 * formatting, use .setDefault....().
 * 
 * You can add descriptive labels to the widgets you add, you can format those
 * labels. You can add regex validators, required fields, and custom widgets all
 * around the widget being added.
 * 
 * You can specify fancy at-least-one-must-not-be-blank requirements on any
 * number of widgets.
 * 
 * @author serge
 */
public class FormatPanel extends VerticalPanel {

	// all defaults of this panel are stored in this structure.
	private FormatPanelDefaults defaults;

	// this stores relationship of widget added to their container.
	private HashMap<Widget, Container> widgetToContainer = new HashMap<Widget, Container>();

	// this stores the validator for widgets.
	private HashMap<Widget, String> widgetToRegex = new HashMap<Widget, String>();

	// this list will store all widgets who have required values
	private ArrayList<Widget> required = new ArrayList<Widget>();

	// the focus listener used for highlights and validation
	private WidgetListener listener = new WidgetListener();

	// this array list keeps track of hidden widgets, we do not need to validate
	// that which is hidden
	private ArrayList<Widget> hidden = new ArrayList<Widget>();

	private ArrayList<Widget> failedRegexOnBlur = new ArrayList<Widget>();

	// widget listener events
	private static final int FOCUS_LOST = 1;
	private static final int KEYUP = 2;
	private static final int ON_CLICK = 3;
	private static final int SYSTEM = 4;

	/**
	 * Creates a new format panel.
	 */
	public FormatPanel() {

		// set up the panel.
		setStyleName("mshab-formatPanel");
		setSpacing(5);
		defaults = new FormatPanelDefaults();
	}

	/**
	 * Adds a widget to the format panel.
	 */
	public void add(Widget w) {

		// create a container
		Container con = new Container(defaults, w);

		// register the widget with the container.
		widgetToContainer.put(w, con);

		// Add focus, click, and key listeners where possible.
		if (w instanceof FocusWidget) {
			((FocusWidget) w).addFocusListener(listener);
		}
		if (w instanceof TextBoxBase) {
			((TextBoxBase) w).addKeyboardListener(listener);
		}
		if (w instanceof CheckBox) {
			((CheckBox) w).addClickListener(listener);
		}
		// adding the container.
		super.add(con);
	}

	/**
	 * Removes a widget and it's container from the panel. Everything associated
	 * with this widget, it's labels, decoration etc, will be removed.
	 * 
	 * @param w
	 *            the widget to remove.
	 */
	public boolean remove(Widget w) {
		Container cont = widgetToContainer.get(w);
		if (cont != null) {
			return super.remove(cont);
		}
		return false;
	}

	/**
	 * This will lightly indent the container of the given widget. How much the
	 * intentation is can be controlled by CSS using ".mshab-container-indent"
	 * 
	 * @param w
	 *            the widget to indent.
	 */
	public void indent(Widget w) {
		Container cont = widgetToContainer.get(w);
		cont.setStyleName("mshab-container-indent");
	}

	/**
	 * Adds a line of whitespace for formatting purposes
	 */
	public void addBreak() {
		Label br = new Label("");
		br.setStyleName("mshab-break");
		add(br);
	}

	/**
	 * Creates a label with the given title for the specified widget.
	 * 
	 * @param widget
	 *            the widget who's title to set.
	 * @param title
	 *            the title to set.
	 */
	public void setLabel(Widget widget, String title) {
		Container cont = widgetToContainer.get(widget);
		cont.setLabel(title);
	}

	/**
	 * Creates a label with the given title for the speficied widget in a given
	 * direction
	 * 
	 * @param widget
	 *            the widget who's title to set.
	 * @param title
	 *            the title to set.
	 * @param dir
	 *            the orientation of the title with respect to the widget.
	 */
	public void setLabel(Widget widget, String title, Direction dir) {
		Container cont = widgetToContainer.get(widget);
		cont.addLabel(title, dir);
	}

	/**
	 * This method is similar to setLabel() though it takes in a widget instead
	 * of a string for a label. Furthermore, several widgets can be added in the
	 * same directional slot.
	 * 
	 * @param widget
	 *            the main widget to which to add a surrounding widget
	 * @param header
	 *            the surrounding widget.
	 * @param direction
	 *            the orientation of the surrounding widget with respect to
	 *            original widget
	 */
	public void addDecoration(Widget w, Widget header, Direction direction) {
		Container cont = widgetToContainer.get(w);
		cont.addWidget(header, direction);
	}

	/**
	 * Set's the specified widget as required.
	 * 
	 * @param w
	 *            the widget to set as required.
	 */
	public void setRequired(Widget w) {
		required.add(w);

		// set up the container of the widget
		Container cont = widgetToContainer.get(w);
		cont.addRequiredNotification();
	}

	/**
	 * Set's the specified widget as required.
	 * 
	 * @param w
	 *            the widget to set as required.
	 * @param message
	 *            the message to display should be left blank
	 */
	public void setRequired(Widget w, String message) {
		required.add(w);

		// set up the container of the widget
		Container cont = widgetToContainer.get(w);
		cont.addRequiredNotification(message);
	}

	/**
	 * Sets a validator that will fire off if the entered text is not what is
	 * expected
	 * 
	 * @param widget
	 *            the widget which to check against
	 * @param regex
	 * @param errorMessage
	 */
	public void setValidator(Widget widget, String regex, String errorMessage) {
		widgetToRegex.put(widget, regex);

		// set up the container to show the error message should it occur.
		Container cont = widgetToContainer.get(widget);
		cont.addValidator(errorMessage);
	}

	/**
	 * This focus listener will change the css to allow highlighting as well as
	 * handle validation.
	 * 
	 * @author me
	 * 
	 */
	private class WidgetListener implements FocusListener, KeyboardListener,
			ClickListener {

		public void onFocus(Widget sender) {
			// assign a focus css to append to the current css style.
			addCSSPostFix(sender, "-focus");
		}

		public void onLostFocus(Widget sender) {
			validate(sender, FOCUS_LOST);
		}

		public void onKeyDown(Widget sender, char keyCode, int modifiers) {
		}

		/**
		 * User hit the enter key on a text area, we WILL validate the field,
		 * thus it get's marked as SYSTEM.
		 */
		public void onKeyPress(Widget sender, char keyCode, int modifiers) {

		}

		public void onKeyUp(Widget sender, char keyCode, int modifiers) {
			if (((int) keyCode) == 13) {
				validate(sender, SYSTEM);
				((FocusWidget)sender).setFocus(false);
			} else {
				validate(sender, KEYUP);
			}
		}

		public void onClick(Widget sender) {
			validate(sender, ON_CLICK);
		}
	}

	/**
	 * Checks a specific widget for information that was required and not filled
	 * in, and for regex formatting of that information
	 * 
	 * @param widget
	 */
	private boolean validate(Widget widget, int event) {

		// get the container associated with widget.
		Container cont = widgetToContainer.get(widget);

		// Remove the focus css.
		// EXCEPTION: if this is a text field with a defined regex and the event
		// is KEYUP don't do it. Regex is not validated as the user is typing
		// unless they screwed up before, so we don't want to lose widget focus
		// while they do their typing.
		if (!(widget instanceof TextBoxBase
				&& widgetToRegex.containsKey(widget) && event == KEYUP)) {
			removeCSSPostFix(widget, "-focus");
		}

		// remove failed CSS, we will add it again if something is wrong.
		removeCSSPostFix(widget, "-failed");

		// assume it's fixed, if not, it's checked right below.
		cont.hideRequiredNotifier();
		cont.hideValidationNotifier();

		// check if field is required - set as failed if nothing is there.
		boolean failed_required = false;
		if (required.contains(widget) && BlankChecker.isBlank(widget)) {
			addCSSPostFix(widget, "-failed");
			cont.showRequiredNotifier();
			failed_required = true;
		}

		/*
		 * Check if the field has any validators. We don't bother validating if
		 * it's blank and we are displaying a "this is a required" box.
		 * 
		 * Only validate regex on focus lost events UNLESS this widget
		 * previously failed on a focus lost and the user is now typing (KEYUP)
		 * to correct the problem.
		 */
		boolean failed_validation = false;
		if (widgetToRegex.containsKey(widget)
				&& widget instanceof TextBoxBase
				&& !failed_required
				&& (event == FOCUS_LOST || event == SYSTEM || (event == KEYUP && failedRegexOnBlur
						.contains(widget)))) {
			String regex = widgetToRegex.get(widget);
			String value = ((TextBoxBase) widget).getText();

			// if it does not match.
			if (!value.matches(regex)) {
				addCSSPostFix(widget, "-failed");
				cont.showValidationNotifier();
				failed_validation = true;

				// if failed on focus lost, remember widget
				if (event == FOCUS_LOST) {
					failedRegexOnBlur.add(widget);
				}

			}
			// regex matches.
			else {
				// remove from bad list
				failedRegexOnBlur.remove(widget);
			}
		}
		return !failed_required && !failed_validation;
	}

	/**
	 * Appends a given string to the end of whatever CSS style the given widget
	 * has
	 * 
	 * @param w
	 *            the widget to which to apply the CSS change
	 * @param val
	 *            the value to append to the CSS style name of the given widget
	 */
	private void addCSSPostFix(Widget w, String val) {
		String old_style = w.getStyleName();
		
		if (!old_style.endsWith(val) && !old_style.endsWith("-failed")) {
			
			// remove focus if adding a -fail
			removeCSSPostFix(w, "-focus");
			old_style = w.getStyleName();
			
			// apply the style.
			w.setStyleName(old_style + val);
		}
	}

	/**
	 * Remove a string from the end of the CSS of the given widget.
	 * 
	 * @param w
	 *            the widget from who's CSS style to remove the given ending
	 *            string
	 * @param val
	 *            the string value which to remove from the given widget's CSS
	 *            value
	 */
	private void removeCSSPostFix(Widget w, String val) {
		String old_style = w.getStyleName();
		if (old_style.endsWith(val)) {
			w.setStyleName(old_style.substring(0, old_style.indexOf(val)));
		}
	}

	/**
	 * Run validation on all the registered widgets to make sure all required
	 * fields are filled and everything that has a regex attached to it does in
	 * fact validate well.
	 * 
	 * This will trigger any widgets's notifiers which are not ok.
	 * 
	 * @return true if everything is ok, false if there is at least one widget
	 *         that is required and not filled, or does not pass regex
	 *         validation
	 */
	public boolean validate() {
		boolean passed = true;

		// walk though every widget that we have and validate it.
		Iterator it = widgetToContainer.keySet().iterator();
		while (it.hasNext()) {
			Widget current_widget = (Widget) it.next();

			// we do not validate that which is hidden.
			if (!hidden.contains(current_widget)) {
				boolean result = validate(current_widget, SYSTEM);
				passed &= result;
			}
		}
		return passed;
	}

	/**
	 * Hides the container of the associated widget.
	 * 
	 * @param widget
	 *            the widget which is to be hidden
	 */
	public void hide(Widget widget) {
		// get the container associated with widget.
		Container cont = widgetToContainer.get(widget);
		cont.setVisible(false);
		hidden.add(widget);
	}

	/**
	 * Shows the given widget if it is hidden
	 * 
	 * @param widget
	 *            the widget to be shown.
	 */
	public void show(Widget widget) {
		// get the container associated with widget.
		Container cont = widgetToContainer.get(widget);
		cont.setVisible(true);
		hidden.remove(widget);
	}

	/**
	 * Creates a show rule based on another widget being set.
	 * 
	 * @param original
	 *            the widget to show
	 * @param conditional
	 *            the widget that must be set.
	 */
	public void showIfSet(Widget original, Widget conditional) {
		// get the container associated with widget.
		Container cont = widgetToContainer.get(original);
		VisibilityManager.getInstance().addShowRule(cont, conditional);
	}

	/**
	 * Creates a hide rule based on another widget being set.
	 * 
	 * @param original
	 *            the widget to hide
	 * @param conditional
	 *            the widget that must be set.
	 */
	public void hideIfSet(Widget original, Widget conditional) {
		// get the container associated with widget.
		Container cont = widgetToContainer.get(original);
		VisibilityManager.getInstance().addHideRule(cont, conditional);
	}

	/**
	 * Creates a show/hide rule based on a value of a widget.
	 * 
	 * @param original
	 *            the widget to show/hide
	 * @param conditional
	 *            the widget who's value determines visibility
	 * @param regex
	 *            the condition must be met to show, else original widget is
	 *            hiden.
	 */
	public void showHideIfValue(Widget original, Widget conditional,
			String regex) {

		// get the container associated with widget.
		Container cont = widgetToContainer.get(original);
		VisibilityManager.getInstance().addValueRule(cont, conditional, regex);
	}

	/* Boring setters and getters */

	public void setDefaultLabelPosition(Direction defaultLabelPosition) {
		defaults.setLabelPosition(defaultLabelPosition);
	}

	public void setDefaultLeftLabelWidth(int defaultLeftLabelWidth) {
		defaults.setLeftLabelWidth(defaultLeftLabelWidth);
	}
}
